/*
 * Copyright 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.compose.ui.test

import android.content.Context
import android.hardware.input.InputManager
import android.view.InputEvent
import android.view.KeyCharacterMap
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.MotionEvent.ACTION_BUTTON_PRESS
import android.view.MotionEvent.ACTION_BUTTON_RELEASE
import android.view.MotionEvent.ACTION_CANCEL
import android.view.MotionEvent.ACTION_DOWN
import android.view.MotionEvent.ACTION_HOVER_ENTER
import android.view.MotionEvent.ACTION_HOVER_EXIT
import android.view.MotionEvent.ACTION_HOVER_MOVE
import android.view.MotionEvent.ACTION_MOVE
import android.view.MotionEvent.ACTION_POINTER_DOWN
import android.view.MotionEvent.ACTION_POINTER_INDEX_SHIFT
import android.view.MotionEvent.ACTION_POINTER_UP
import android.view.MotionEvent.ACTION_SCROLL
import android.view.MotionEvent.ACTION_UP
import android.view.MotionEvent.PointerCoords
import android.view.MotionEvent.PointerProperties
import android.view.MotionEvent.TOOL_TYPE_UNKNOWN
import android.view.ViewConfiguration
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.nativeKeyCode
import androidx.compose.ui.node.RootForTest
import androidx.compose.ui.platform.ViewRootForTest
import androidx.compose.ui.test.platform.makeSynchronizedObject
import androidx.compose.ui.test.platform.synchronized
import androidx.core.view.InputDeviceCompat.SOURCE_MOUSE
import androidx.core.view.InputDeviceCompat.SOURCE_ROTARY_ENCODER
import androidx.core.view.InputDeviceCompat.SOURCE_TOUCHSCREEN
import androidx.core.view.MotionEventCompat.AXIS_SCROLL
import androidx.core.view.ViewConfigurationCompat.getScaledHorizontalScrollFactor
import androidx.core.view.ViewConfigurationCompat.getScaledVerticalScrollFactor

private val MouseAsTouchEvents = listOf(ACTION_DOWN, ACTION_MOVE, ACTION_UP)

internal actual fun createInputDispatcher(
    testContext: TestContext,
    root: RootForTest,
): InputDispatcher {
    require(root is ViewRootForTest) {
        "InputDispatcher only supports dispatching to ViewRootForTest, not to " +
            root::class.java.simpleName
    }
    val view = root.view
    return AndroidInputDispatcher(testContext, root) {
        when (it) {
            is KeyEvent -> view.dispatchKeyEvent(it)
            is MotionEvent -> {
                when (it.source) {
                    SOURCE_TOUCHSCREEN -> view.dispatchTouchEvent(it)
                    SOURCE_ROTARY_ENCODER -> view.dispatchGenericMotionEvent(it)
                    SOURCE_MOUSE ->
                        when (it.action) {
                            in MouseAsTouchEvents -> view.dispatchTouchEvent(it)
                            else -> view.dispatchGenericMotionEvent(it)
                        }
                    else ->
                        throw IllegalArgumentException(
                            "Can't dispatch MotionEvents with source ${it.source}"
                        )
                }
            }
        }
    }
}

internal class AndroidInputDispatcher(
    private val testContext: TestContext,
    private val root: ViewRootForTest,
    private val sendEvent: (InputEvent) -> Unit,
) : InputDispatcher(testContext, root) {

    private val batchLock = makeSynchronizedObject()
    private var batchedEvents = mutableListOf<InputEvent>()
    private var disposed = false
    private var currentClockTime = currentTime

    // TODO(b/214439478): Find out if we should add these values to Compose's ViewConfiguration.
    // Scroll factors for Rotary Input.
    private val verticalScrollFactor: Float by lazy {
        val context = root.view.context
        val config = ViewConfiguration.get(context)
        getScaledVerticalScrollFactor(config, context)
    }
    private val horizontalScrollFactor: Float by lazy {
        val context = root.view.context
        val config = ViewConfiguration.get(context)
        getScaledHorizontalScrollFactor(config, context)
    }

    override fun PartialGesture.enqueueDown(pointerId: Int) {
        enqueueTouchEvent(
            if (lastPositions.size == 1) ACTION_DOWN else ACTION_POINTER_DOWN,
            lastPositions.keys.sorted().indexOf(pointerId),
        )
    }

    override fun PartialGesture.enqueueMove() {
        enqueueTouchEvent(ACTION_MOVE, 0)
    }

    override fun PartialGesture.enqueueMoves(
        relativeHistoricalTimes: List<Long>,
        historicalCoordinates: List<List<Offset>>,
    ) {
        val entries = lastPositions.entries.sortedBy { it.key }
        val absoluteHistoricalTimes = relativeHistoricalTimes.map { currentTime + it }
        enqueueTouchEvent(
            downTime = downTime,
            action = ACTION_MOVE,
            actionIndex = 0,
            pointerIds = List(entries.size) { entries[it].key },
            eventTimes = absoluteHistoricalTimes + listOf(currentTime),
            coordinates =
                List(entries.size) { historicalCoordinates[it] + listOf(entries[it].value) },
        )
    }

    override fun PartialGesture.enqueueUp(pointerId: Int) {
        enqueueTouchEvent(
            if (lastPositions.size == 1) ACTION_UP else ACTION_POINTER_UP,
            lastPositions.keys.sorted().indexOf(pointerId),
        )
    }

    override fun PartialGesture.enqueueCancel() {
        enqueueTouchEvent(ACTION_CANCEL, 0)
    }

    override fun MouseInputState.enqueuePress(buttonId: Int) {
        enqueueMouseEvent(if (hasOneButtonPressed) ACTION_DOWN else ACTION_MOVE)
        if (isWithinRootBounds(currentMousePosition)) {
            enqueueMouseEvent(ACTION_BUTTON_PRESS)
        }
    }

    override fun MouseInputState.enqueueMove() {
        if (isWithinRootBounds(currentMousePosition)) {
            enqueueMouseEvent(if (isEntered) ACTION_HOVER_MOVE else ACTION_MOVE)
        } else if (hasAnyButtonPressed) {
            enqueueMouseEvent(ACTION_MOVE)
        }
    }

    override fun MouseInputState.enqueueRelease(buttonId: Int) {
        if (isWithinRootBounds(currentMousePosition)) {
            enqueueMouseEvent(ACTION_BUTTON_RELEASE)
        }
        enqueueMouseEvent(if (hasNoButtonsPressed) ACTION_UP else ACTION_MOVE)
    }

    override fun MouseInputState.enqueueEnter() {
        if (isWithinRootBounds(currentMousePosition)) {
            enqueueMouseEvent(ACTION_HOVER_ENTER)
        }
    }

    override fun MouseInputState.enqueueExit() {
        enqueueMouseEvent(ACTION_HOVER_EXIT)
    }

    override fun MouseInputState.enqueueCancel() {
        enqueueMouseEvent(ACTION_CANCEL)
    }

    override fun MouseInputState.enqueueScroll(delta: Float, scrollWheel: ScrollWheel) {
        enqueueMouseEvent(
            ACTION_SCROLL,
            // We invert vertical scrolling to align with another platforms.
            // Vertical scrolling on desktop/web have opposite sign.
            if (scrollWheel == ScrollWheel.Vertical) -delta else delta,
            when (scrollWheel) {
                ScrollWheel.Horizontal -> MotionEvent.AXIS_HSCROLL
                ScrollWheel.Vertical -> MotionEvent.AXIS_VSCROLL
                else -> -1
            },
        )
    }

    override fun MouseInputState.enqueueScroll(offset: Offset) {
        enqueueMouseEvent(
            downTime = downTime,
            eventTime = currentTime,
            action = ACTION_SCROLL,
            coordinate = lastPosition,
            metaState = keyInputState.constructMetaState(),
            buttonState = pressedButtons.fold(0) { state, buttonId -> state or buttonId },
            // We invert vertical scrolling to align with another platforms.
            // Vertical scrolling on desktop/web have opposite sign.
            delta = offset.copy(y = -offset.y),
        )
    }

    fun KeyInputState.constructMetaState(): Int {

        fun genState(key: Key, mask: Int) = if (isKeyDown(key)) mask else 0

        return (if (capsLockOn) KeyEvent.META_CAPS_LOCK_ON else 0) or
            (if (numLockOn) KeyEvent.META_NUM_LOCK_ON else 0) or
            (if (scrollLockOn) KeyEvent.META_SCROLL_LOCK_ON else 0) or
            genState(Key.Function, KeyEvent.META_FUNCTION_ON) or
            genState(Key.CtrlLeft, KeyEvent.META_CTRL_LEFT_ON or KeyEvent.META_CTRL_ON) or
            genState(Key.CtrlRight, KeyEvent.META_CTRL_RIGHT_ON or KeyEvent.META_CTRL_ON) or
            genState(Key.AltLeft, KeyEvent.META_ALT_LEFT_ON or KeyEvent.META_ALT_ON) or
            genState(Key.AltRight, KeyEvent.META_ALT_RIGHT_ON or KeyEvent.META_ALT_ON) or
            genState(Key.MetaLeft, KeyEvent.META_META_LEFT_ON or KeyEvent.META_META_ON) or
            genState(Key.MetaRight, KeyEvent.META_META_RIGHT_ON or KeyEvent.META_META_ON) or
            genState(Key.ShiftLeft, KeyEvent.META_SHIFT_LEFT_ON or KeyEvent.META_SHIFT_ON) or
            genState(Key.ShiftRight, KeyEvent.META_SHIFT_RIGHT_ON or KeyEvent.META_SHIFT_ON)
    }

    override fun KeyInputState.enqueueDown(key: Key) =
        enqueueKeyEvent(KeyEvent.ACTION_DOWN, key.nativeKeyCode, constructMetaState())

    override fun KeyInputState.enqueueUp(key: Key) =
        enqueueKeyEvent(KeyEvent.ACTION_UP, key.nativeKeyCode, constructMetaState())

    /**
     * Generates a MotionEvent with the given [action] and [actionIndex], adding all pointers that
     * are currently in the gesture, and adds the MotionEvent to the batch.
     *
     * @see MotionEvent.getAction
     * @see MotionEvent.getActionIndex
     */
    private fun PartialGesture.enqueueTouchEvent(action: Int, actionIndex: Int) {
        val entries = lastPositions.entries.sortedBy { it.key }
        enqueueTouchEvent(
            downTime = downTime,
            action = action,
            actionIndex = actionIndex,
            pointerIds = List(entries.size) { entries[it].key },
            eventTimes = listOf(currentTime),
            coordinates = List(entries.size) { listOf(entries[it].value) },
        )
    }

    /** Generates an event with the given parameters. */
    private fun enqueueTouchEvent(
        downTime: Long,
        action: Int,
        actionIndex: Int,
        pointerIds: List<Int>,
        eventTimes: List<Long>,
        coordinates: List<List<Offset>>,
    ) {
        check(coordinates.size == pointerIds.size) {
            "Coordinates size should equal pointerIds size " +
                "(was: ${coordinates.size}, ${pointerIds.size})"
        }
        repeat(pointerIds.size) { pointerIndex ->
            check(eventTimes.size == coordinates[pointerIndex].size) {
                "Historical eventTimes size should equal coordinates[$pointerIndex] size " +
                    "(was: ${eventTimes.size}, ${coordinates[pointerIndex].size})"
            }
        }

        synchronized(batchLock) {
            ensureNotDisposed {
                "Can't enqueue touch event (" +
                    "downTime=$downTime, " +
                    "action=$action, " +
                    "actionIndex=$actionIndex, " +
                    "pointerIds=$pointerIds, " +
                    "eventTimes=$eventTimes, " +
                    "coordinates=$coordinates)"
            }
            val positionInScreen = run {
                val array = intArrayOf(0, 0)
                root.view.getLocationOnScreen(array)
                Offset(array[0].toFloat(), array[1].toFloat())
            }
            val motionEvent =
                MotionEvent.obtain(
                        /* downTime = */ downTime,
                        /* eventTime = */ eventTimes[0],
                        /* action = */ action + (actionIndex shl ACTION_POINTER_INDEX_SHIFT),
                        /* pointerCount = */ coordinates.size,
                        /* pointerProperties = */ Array(coordinates.size) { pointerIndex ->
                            PointerProperties().apply {
                                id = pointerIds[pointerIndex]
                                toolType = MotionEvent.TOOL_TYPE_FINGER
                            }
                        },
                        /* pointerCoords = */ Array(coordinates.size) { pointerIndex ->
                            PointerCoords().apply {
                                val startOffset = coordinates[pointerIndex][0]

                                // Allows for non-valid numbers/Offsets to be passed along to
                                // Compose to
                                // test if it handles them properly (versus breaking here and we not
                                // knowing
                                // if Compose properly handles these values).
                                x =
                                    if (startOffset.isValid()) {
                                        positionInScreen.x + startOffset.x
                                    } else {
                                        Float.NaN
                                    }

                                y =
                                    if (startOffset.isValid()) {
                                        positionInScreen.y + startOffset.y
                                    } else {
                                        Float.NaN
                                    }
                            }
                        },
                        /* metaState = */ 0,
                        /* buttonState = */ 0,
                        /* xPrecision = */ 1f,
                        /* yPrecision = */ 1f,
                        /* deviceId = */ 0,
                        /* edgeFlags = */ 0,
                        /* source = */ SOURCE_TOUCHSCREEN,
                        /* flags = */ 0,
                    )
                    .apply {
                        // The current time & coordinates are the last element in the lists, and
                        // need to
                        // be passed into the final addBatch call. If there are no historical
                        // events,
                        // the list sizes are 1 and we don't need to call addBatch at all.
                        for (timeIndex in 1 until eventTimes.size) {
                            addBatch(
                                /* eventTime = */ eventTimes[timeIndex],
                                /* pointerCoords = */ Array(coordinates.size) { pointerIndex ->
                                    PointerCoords().apply {
                                        val currentOffset = coordinates[pointerIndex][timeIndex]

                                        // Allows for non-valid numbers/Offsets to be passed along
                                        // to
                                        // Compose to test if it handles them properly (versus
                                        // breaking
                                        // here and we not knowing if Compose properly handles these
                                        // values).
                                        x =
                                            if (currentOffset.isValid()) {
                                                positionInScreen.x + currentOffset.x
                                            } else {
                                                Float.NaN
                                            }

                                        y =
                                            if (currentOffset.isValid()) {
                                                positionInScreen.y + currentOffset.y
                                            } else {
                                                Float.NaN
                                            }
                                    }
                                },
                                /* metaState = */ 0,
                            )
                        }
                        offsetLocation(-positionInScreen.x, -positionInScreen.y)
                    }

            batchedEvents.add(motionEvent)
        }
    }

    private fun MouseInputState.enqueueMouseEvent(action: Int, delta: Float = 0f, axis: Int = -1) {
        enqueueMouseEvent(
            downTime = downTime,
            eventTime = currentTime,
            action = action,
            coordinate = lastPosition,
            metaState = keyInputState.constructMetaState(),
            buttonState = pressedButtons.fold(0) { state, buttonId -> state or buttonId },
            axis = axis,
            axisDelta = delta,
        )
    }

    private fun enqueueMouseEvent(
        downTime: Long,
        eventTime: Long,
        action: Int,
        coordinate: Offset,
        metaState: Int,
        buttonState: Int,
        axis: Int = -1,
        axisDelta: Float = 0f,
    ) {
        synchronized(batchLock) {
            ensureNotDisposed {
                "Can't enqueue mouse event (" +
                    "downTime=$downTime, " +
                    "eventTime=$eventTime, " +
                    "action=$action, " +
                    "coordinate=$coordinate, " +
                    "metaState=$metaState, " +
                    "buttonState=$buttonState, " +
                    "axis=$axis, " +
                    "axisDelta=$axisDelta)"
            }
            val positionInScreen = run {
                val array = intArrayOf(0, 0)
                root.view.getLocationOnScreen(array)
                Offset(array[0].toFloat(), array[1].toFloat())
            }
            batchedEvents.add(
                MotionEvent.obtain(
                        /* downTime = */ downTime,
                        /* eventTime = */ eventTime,
                        /* action = */ action,
                        /* pointerCount = */ 1,
                        /* pointerProperties = */ arrayOf(
                            PointerProperties().apply {
                                id = 0
                                toolType = MotionEvent.TOOL_TYPE_MOUSE
                            }
                        ),
                        /* pointerCoords = */ arrayOf(
                            PointerCoords().apply {
                                x = positionInScreen.x + coordinate.x
                                y = positionInScreen.y + coordinate.y
                                if (axis != -1) {
                                    setAxisValue(axis, axisDelta)
                                }
                            }
                        ),
                        /* metaState = */ metaState,
                        /* buttonState = */ buttonState,
                        /* xPrecision = */ 1f,
                        /* yPrecision = */ 1f,
                        /* deviceId = */ 0,
                        /* edgeFlags = */ 0,
                        /* source = */ SOURCE_MOUSE,
                        /* flags = */ 0,
                    )
                    .apply { offsetLocation(-positionInScreen.x, -positionInScreen.y) }
            )
        }
    }

    private fun enqueueMouseEvent(
        downTime: Long,
        eventTime: Long,
        action: Int,
        coordinate: Offset,
        metaState: Int,
        buttonState: Int,
        axis: Int = -1,
        delta: Offset,
    ) {
        synchronized(batchLock) {
            ensureNotDisposed {
                "Can't enqueue mouse event (" +
                    "downTime=$downTime, " +
                    "eventTime=$eventTime, " +
                    "action=$action, " +
                    "coordinate=$coordinate, " +
                    "metaState=$metaState, " +
                    "buttonState=$buttonState, " +
                    "axis=$axis, " +
                    "delta=$delta)"
            }
            val positionInScreen = run {
                val array = intArrayOf(0, 0)
                root.view.getLocationOnScreen(array)
                Offset(array[0].toFloat(), array[1].toFloat())
            }
            batchedEvents.add(
                MotionEvent.obtain(
                        /* downTime = */ downTime,
                        /* eventTime = */ eventTime,
                        /* action = */ action,
                        /* pointerCount = */ 1,
                        /* pointerProperties = */ arrayOf(
                            PointerProperties().apply {
                                id = 0
                                toolType = MotionEvent.TOOL_TYPE_MOUSE
                            }
                        ),
                        /* pointerCoords = */ arrayOf(
                            PointerCoords().apply {
                                x = positionInScreen.x + coordinate.x
                                y = positionInScreen.y + coordinate.y
                                setAxisValue(MotionEvent.AXIS_HSCROLL, delta.x)
                                setAxisValue(MotionEvent.AXIS_VSCROLL, delta.y)
                            }
                        ),
                        /* metaState = */ metaState,
                        /* buttonState = */ buttonState,
                        /* xPrecision = */ 1f,
                        /* yPrecision = */ 1f,
                        /* deviceId = */ 0,
                        /* edgeFlags = */ 0,
                        /* source = */ SOURCE_MOUSE,
                        /* flags = */ 0,
                    )
                    .apply { offsetLocation(-positionInScreen.x, -positionInScreen.y) }
            )
        }
    }

    override fun RotaryInputState.enqueueRotaryScrollHorizontally(horizontalScrollPixels: Float) {
        enqueueRotaryScrollEvent(
            eventTime = currentTime,
            scrollPixels = -horizontalScrollPixels / horizontalScrollFactor,
        )
    }

    override fun RotaryInputState.enqueueRotaryScrollVertically(verticalScrollPixels: Float) {
        enqueueRotaryScrollEvent(
            eventTime = currentTime,
            scrollPixels = -verticalScrollPixels / verticalScrollFactor,
        )
    }

    private fun enqueueRotaryScrollEvent(eventTime: Long, scrollPixels: Float) {
        synchronized(batchLock) {
            ensureNotDisposed {
                "Can't enqueue rotary scroll event (" +
                    "eventTime=$eventTime, " +
                    "scrollDelta=$scrollPixels)"
            }
            batchedEvents.add(
                MotionEvent.obtain(
                    /* downTime = */ 0,
                    /* eventTime = */ eventTime,
                    /* action = */ ACTION_SCROLL,
                    /* pointerCount = */ 1,
                    /* pointerProperties = */ arrayOf(
                        PointerProperties().apply {
                            id = 0
                            toolType = TOOL_TYPE_UNKNOWN
                        }
                    ),
                    /* pointerCoords = */ arrayOf(
                        PointerCoords().apply { setAxisValue(AXIS_SCROLL, scrollPixels) }
                    ),
                    /* metaState = */ 0,
                    /* buttonState = */ 0,
                    /* xPrecision = */ 1f,
                    /* yPrecision = */ 1f,
                    /* deviceId = */ findInputDevice(root.view.context, SOURCE_ROTARY_ENCODER),
                    /* edgeFlags = */ 0,
                    /* source = */ SOURCE_ROTARY_ENCODER,
                    /* flags = */ 0,
                )
            )
        }
    }

    /**
     * Generates a KeyEvent with the given [action] and [keyCode] and adds the KeyEvent to the
     * batch.
     *
     * @see KeyEvent.getAction
     * @see KeyEvent.getKeyCode
     */
    private fun KeyInputState.enqueueKeyEvent(action: Int, keyCode: Int, metaState: Int) {
        enqueueKeyEvent(
            downTime = downTime,
            eventTime = currentTime,
            action = action,
            code = keyCode,
            repeat = repeatCount,
            metaState = metaState,
        )
    }

    /** Generates a key event with the given parameters. */
    private fun enqueueKeyEvent(
        downTime: Long,
        eventTime: Long,
        action: Int,
        code: Int,
        repeat: Int,
        metaState: Int,
    ) {
        synchronized(batchLock) {
            ensureNotDisposed {
                "Can't enqueue key event (" +
                    "downTime=$downTime, " +
                    "eventTime=$eventTime, " +
                    "action=$action, " +
                    "code=$code, " +
                    "repeat=$repeat, " +
                    "metaState=$metaState)"
            }

            val keyEvent =
                KeyEvent(
                    /* downTime = */ downTime,
                    /* eventTime = */ eventTime,
                    /* action = */ action,
                    /* code = */ code,
                    /* repeat = */ repeat,
                    /* metaState = */ metaState,
                    /* deviceId = */ KeyCharacterMap.VIRTUAL_KEYBOARD,
                    /* scancode = */ 0,
                )

            batchedEvents.add(keyEvent)
        }
    }

    override fun flush() {
        // Must inject on the main thread, because it might modify View properties
        testContext.testOwner.runOnUiThread {
            val events =
                synchronized(batchLock) {
                    ensureNotDisposed { "Can't flush events" }
                    mutableListOf<InputEvent>().apply {
                        addAll(batchedEvents)
                        batchedEvents.clear()
                    }
                }

            events.forEach { event ->
                // Before injecting the next event, pump the clock
                // by the difference between this and the last event
                advanceClockTime(event.eventTime - currentClockTime)
                currentClockTime = event.eventTime
                sendAndRecycleEvent(event)
            }
            // Run all due tasks to make sure the last event is being handled.
            // This is necessary in case we're on a confined scheduler.
            testContext.testOwner.runCurrent()
        }
    }

    private fun advanceClockTime(millis: Long) {
        // Don't bother advancing the clock if there's nothing to advance
        if (millis > 0) {
            testContext.testOwner.mainClock.advanceTimeBy(millis, ignoreFrameDuration = true)
        }
    }

    private fun ensureNotDisposed(lazyMessage: () -> String) {
        check(!disposed) { "${lazyMessage()}, AndroidInputDispatcher has already been disposed" }
    }

    override fun onDispose() {
        synchronized(batchLock) {
            if (!disposed) {
                disposed = true
                batchedEvents.forEach { recycleEventIfPossible(it) }
            }
        }
    }

    /** Sends and recycles the given [event]. */
    private fun sendAndRecycleEvent(event: InputEvent) {
        try {
            sendEvent(event)
        } finally {
            recycleEventIfPossible(event)
        }
    }

    /**
     * Recycles the [event] if it is a [MotionEvent]. There is no notion of recycling a [KeyEvent].
     */
    private fun recycleEventIfPossible(event: InputEvent) {
        (event as? MotionEvent)?.recycle()
    }

    private fun findInputDevice(context: Context, source: Int): Int {
        with(context.getSystemService(Context.INPUT_SERVICE) as InputManager) {
            inputDeviceIds.forEach { deviceId ->
                getInputDevice(deviceId)?.apply {
                    motionRanges
                        .find { it.source == source }
                        ?.let {
                            return deviceId
                        }
                }
            }
        }
        return 0
    }
}
